<template>
    <div v-loading="firstLoading">
        <div v-if="defaultButton">
            <el-button icon="Refresh" v-if="!showTail" @click="getContent(false)">
                {{ $t('commons.button.refresh') }}
            </el-button>
            <el-checkbox
                border
                :disabled="isTailDisabled"
                v-model="tailLog"
                class="float-left"
                @change="changeTail(false)"
                v-if="showTail"
            >
                {{ $t('commons.button.watch') }}
            </el-checkbox>
            <el-button
                class="ml-2.5"
                v-if="showDownload"
                @click="onDownload"
                icon="Download"
                :disabled="logs.length === 0"
            >
                {{ $t('commons.button.download') }}
            </el-button>
            <span v-if="$slots.button" class="ml-2.5">
                <slot name="button"></slot>
            </span>
        </div>
        <div class="log-container" ref="logContainer" @scroll="onScroll" :style="containerStyle">
            <div class="log-spacer" :style="{ height: `${totalHeight}px` }"></div>
            <div class="log-viewport" :style="{ transform: `translateY(${offsetY}px)` }">
                <div
                    v-for="(log, index) in visibleLogs"
                    :key="`${startIndex + index}-${log}`"
                    class="log-item"
                    :style="{ height: `${logHeight}px` }"
                >
                    <hightlight :log="log" :type="config.colorMode ?? 'nginx'"></hightlight>
                </div>
            </div>
            <hightlight v-if="logs.length === 0" :log="$t('commons.log.noLog')" type="system"></hightlight>
        </div>
    </div>
</template>
<script lang="ts" setup>
import { nextTick, onMounted, onUnmounted, reactive, ref, computed } from 'vue';
import { downloadFile } from '@/utils/util';
import { readByLine } from '@/api/modules/files';
import { GlobalStore } from '@/store';
import bus from '@/global/bus';
import hightlight from '@/components/log/custom-hightlight/index.vue';
const globalStore = GlobalStore();

interface LogProps {
    id?: number;
    type?: string;
    name?: string;
    tail?: boolean;
    taskID?: string;
    colorMode?: string;
    taskType?: string;
    taskOperate?: string;
    resourceID?: number;

    operateNode?: string;
}

const props = defineProps({
    config: {
        type: Object as () => LogProps | null,
        default: () => ({
            id: 0,
            type: '',
            name: '',
            tail: false,
            colorMode: 'nginx',
            taskType: '',
            taskOperate: '',
            resourceID: 0,
            taskID: '',

            operateNode: '',
        }),
    },
    defaultButton: {
        type: Boolean,
        default: true,
    },
    loading: {
        type: Boolean,
        default: true,
    },
    hasContent: {
        type: Boolean,
        default: false,
    },
    heightDiff: {
        type: Number,
        default: 420,
    },
    showTail: {
        type: Boolean,
        default: true,
    },
    showDownload: {
        type: Boolean,
        default: true,
    },
});
const stopSignals = [
    'docker-compose up failed!',
    'docker-compose up successful!',
    'image build failed!',
    'image build successful!',
    'image pull failed!',
    'image pull successful!',
    'image push failed!',
    'image push successful!',
    '[TASK-END]',
];
const emit = defineEmits(['update:loading', 'update:hasContent', 'update:isReading', 'stop-reading']);
const tailLog = ref(true);
const loading = ref(props.loading);
const readReq = reactive({
    id: 0,
    type: '',
    name: '',
    page: 1,
    pageSize: 500,
    latest: false,
    taskID: '',
    taskType: '',
    taskOperate: '',
    resourceID: 0,
});
const isLoading = ref(false);
const end = ref(false);
const lastLogs = ref([]);
const maxPage = ref(0);
const minPage = ref(0);
let timer: ReturnType<typeof setInterval> | null = null;
const logPath = ref('');
const showTail = ref(false);
const isTailDisabled = ref();
const firstLoading = ref(false);
const logs = ref<string[]>([]);
const logContainer = ref<HTMLElement | null>(null);
const logHeight = 23;
const containerHeight = ref(500);
const scrollTop = ref(0);
const lastScrollTop = ref(0);
const totalLines = ref(0);
const stopReading = ref(false);
const totalPages = ref(0);
let resizeObserver: ResizeObserver | null = null;
const isEndOfFile = ref(false);

const totalHeight = computed(() => logs.value.length * logHeight);

const visibleCount = computed(() => {
    const buffer = 5;
    return Math.ceil(containerHeight.value / logHeight) + buffer * 2;
});

const startIndex = computed(() => {
    const buffer = 5;
    const index = Math.floor(scrollTop.value / logHeight) - buffer;
    return Math.max(0, index);
});

const endIndex = computed(() => {
    return Math.min(logs.value.length, startIndex.value + visibleCount.value);
});

const visibleLogs = computed(() => {
    return logs.value.slice(startIndex.value, endIndex.value);
});

const offsetY = computed(() => {
    return startIndex.value * logHeight;
});

const updateContainerHeight = () => {
    if (logContainer.value) {
        const rect = logContainer.value.getBoundingClientRect();
        containerHeight.value = rect.height;
    }
};

const onScroll = async () => {
    if (!logContainer.value) return;

    scrollTop.value = logContainer.value.scrollTop;
    const scrollHeight = logContainer.value.scrollHeight;
    const clientHeight = logContainer.value.clientHeight;

    lastScrollTop.value = scrollTop.value;

    if (isLoading.value) return;

    if (scrollTop.value <= 50 && readReq.page > 1) {
        if (minPage.value <= 1) {
            return;
        }
        readReq.page = minPage.value > 1 ? minPage.value - 1 : 1;
        minPage.value = readReq.page;
        await getContent(true);
        return;
    }
    if (scrollHeight - scrollTop.value - clientHeight <= 50 && !end.value && !isEndOfFile.value) {
        if (readReq.page < maxPage.value) {
            readReq.page = maxPage.value;
            await getContent(false);
        } else if (readReq.page < totalPages.value) {
            maxPage.value++;
            readReq.page = maxPage.value;
            await getContent(false);
        }
    }
};

const changeLoading = () => {
    loading.value = !loading.value;
    emit('update:loading', loading.value);
};

const onDownload = async () => {
    changeLoading();
    downloadFile(logPath.value, globalStore.currentNode);
    changeLoading();
};

const changeTail = (fromOutSide: boolean) => {
    if (fromOutSide) {
        tailLog.value = !tailLog.value;
    }
    if (tailLog.value) {
        timer = setInterval(() => {
            getContent(false);
        }, 1000 * 3);
    } else {
        onCloseLog();
    }
};

const clearLog = (): void => {
    logs.value = [];
    scrollTop.value = 0;
    readReq.page = 1;
    lastLogs.value = [];
};

const getContent = async (pre: boolean) => {
    if (isLoading.value) {
        return;
    }
    if (readReq.page < 1) {
        readReq.page = 1;
    }
    isLoading.value = true;
    emit('update:isReading', true);

    let res;
    try {
        res = await readByLine(readReq, props.config.operateNode || '');
    } catch (error) {
        isLoading.value = false;
        firstLoading.value = false;
        return;
    }

    totalLines.value = res.data.totalLines;

    if (res.data.scope == 'tail') {
        showTail.value = false;
    }

    if (res.data.taskStatus && res.data.taskStatus !== 'Executing') {
        isTailDisabled.value = true;
        tailLog.value = false;
    }

    logPath.value = res.data.path;
    firstLoading.value = false;

    if (!end.value && res.data.end) {
        lastLogs.value = [...logs.value];
    }
    if (res.data.lines && res.data.lines.length > 0) {
        res.data.lines = res.data.lines.map((line) =>
            line.replace(/\\u(\w{4})/g, function (match, grp) {
                return String.fromCharCode(parseInt(grp, 16));
            }),
        );
        const newLogs = res.data.lines;
        if (tailLog.value && newLogs.length === readReq.pageSize && readReq.page < res.data.total) {
            readReq.page++;
        }
        if (
            readReq.type == 'php' &&
            logs.value.length > 0 &&
            newLogs.length > 0 &&
            newLogs[newLogs.length - 1] === logs.value[logs.value.length - 1]
        ) {
            isLoading.value = false;
            return;
        }

        if (stopSignals.some((signal) => newLogs[newLogs.length - 1].endsWith(signal))) {
            onCloseLog();
        }

        if (logs.value.length == 0) {
            logs.value = newLogs;
        } else {
            if (pre) {
                logs.value = [...newLogs, ...logs.value];
            } else {
                if (end.value) {
                    logs.value = [...lastLogs.value, ...newLogs];
                } else {
                    if (newLogs.length > logs.value.length) {
                        logs.value = newLogs;
                    } else {
                        logs.value = [...logs.value, ...newLogs];
                    }
                }
            }
        }

        nextTick(() => {
            if (logContainer.value) {
                if (pre) {
                    if (readReq.page > 1) {
                        const addedLines = newLogs.length;
                        const newScrollPosition = lastScrollTop.value + addedLines * logHeight;
                        logContainer.value.scrollTop = newScrollPosition;
                    }
                } else {
                    logContainer.value.scrollTop = logContainer.value.scrollHeight;
                }
            }
        });
    }

    end.value = res.data.end;
    totalPages.value = res.data.total;
    emit('update:hasContent', logs.value.length > 0);
    if (readReq.latest) {
        readReq.page = res.data.total;
        readReq.latest = false;
        maxPage.value = res.data.total;
        isEndOfFile.value = true;
        if (res.data.lines && res.data.lines.length > 500) {
            minPage.value = res.data.total - 1;
        } else {
            minPage.value = res.data.total;
        }
    } else {
        maxPage.value = Math.max(maxPage.value, readReq.page);
    }
    if (logs.value && logs.value.length > 3000) {
        const removedCount = readReq.pageSize;
        const currentScrollRatio = scrollTop.value / (logs.value.length * logHeight);

        if (pre) {
            logs.value.splice(logs.value.length - removedCount, removedCount);
            if (maxPage.value > 1) {
                maxPage.value--;
            }
        } else {
            isEndOfFile.value = false;
            logs.value.splice(0, removedCount);
            nextTick(() => {
                if (logContainer.value) {
                    const newScrollTop = currentScrollRatio * (logs.value.length * logHeight);
                    logContainer.value.scrollTop = Math.max(0, newScrollTop - removedCount * logHeight);
                }
            });
            if (minPage.value > 1) {
                minPage.value++;
            }
        }
    }
    isLoading.value = false;
};

const onCloseLog = async () => {
    if (stopReading.value) {
        return;
    }
    stopReading.value = true;
    tailLog.value = false;
    if (timer) {
        clearInterval(Number(timer));
        timer = null;
    }
    timer = null;
    emit('stop-reading');
    isLoading.value = false;
    emit('update:isReading', false);
    bus.emit('refreshTask', true);
};

watch(
    () => props.loading,
    (newLoading) => {
        loading.value = newLoading;
    },
);

const init = async () => {
    tailLog.value = false;
    if (props.config.tail) {
        tailLog.value = props.config.tail;
    }
    if (tailLog.value) {
        changeTail(false);
    }
    readReq.latest = true;
    await getContent(false);
    if (readReq.page > 1 && totalPages.value == maxPage.value) {
        readReq.page--;
        await getContent(true);
    }
};

const containerStyle = computed(() => ({
    height: `calc(100vh - ${props.heightDiff}px)`,
}));

onMounted(async () => {
    showTail.value = props.showTail;
    logs.value = [];
    isTailDisabled.value = false;
    firstLoading.value = true;
    readReq.id = props.config.id;
    readReq.type = props.config.type;
    readReq.name = props.config.name;
    readReq.taskID = props.config.taskID;
    readReq.taskType = props.config.taskType;
    readReq.taskOperate = props.config.taskOperate;
    readReq.resourceID = props.config.resourceID;
    await init();

    updateContainerHeight();

    if (logContainer.value) {
        resizeObserver = new ResizeObserver(() => {
            updateContainerHeight();
        });
        resizeObserver.observe(logContainer.value);
    }

    nextTick(() => {
        if (logContainer.value) {
            logContainer.value.scrollTop = logContainer.value.scrollHeight;
        }
    });
});

onUnmounted(() => {
    onCloseLog();
    if (resizeObserver && logContainer.value) {
        resizeObserver.unobserve(logContainer.value);
        resizeObserver.disconnect();
    }
});

defineExpose({ changeTail, onDownload, clearLog });
</script>
<style lang="scss" scoped>
.log-container {
    overflow-y: auto;
    overflow-x: auto;
    position: relative;
    background-color: var(--panel-logs-bg-color);
    margin-top: 10px;
}

.log-spacer {
    position: relative;
    width: 100%;
}

.log-viewport {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    will-change: transform;
    width: max-content;
    min-width: 100%;
}

.log-item {
    min-width: 100%;
    padding: 5px;
    color: #f5f5f5;
    box-sizing: border-box;
    white-space: nowrap;
}

.log-content {
    font-size: 14px;
    line-height: 20px;
}
</style>
